10.2 精通自定义 View 之 Android 画布——Bitmap

返回自定义 View 目录

10.2.1 概述

1. Bitmap 在绘图中的使用

Bitmap 在绘图中相关的使用主要有两种:转换为 BitmapDrawable 对象使用;当做画布使用。

1)转换为 BitmapDrawable 对象使用

就是直接将 Bitmap 转换为 BitmapDrawable 对象,然后转换为 Drawable 使用。如下:

1
2
3
4
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head);
BitmapDrawable bitmapDrawable = new BitmapDrawable(bitmap);
ImageView imageView = findViewById(R.id.image_view);
imageView.setImageDrawable(bitmapDrawable);

2)当做画布使用

在前面的章节中,已经不止一次地将 Bitmap 转换为画布。这里有两种使用方式。
方式一:使用默认画布

1
2
3
4
5
6
7
8
class TestView extends View {
...
public void onDraw(Canvas canvas) {
...
RectF rect = new RectF(120, 10, 210, 100);
canvas.drawRect(rect, paint);
}
}

此处的 Canvas 里保存的就是一个 Bitmap,我们调用 Canvas 的各种绘图函数,最终都是画在这个 Bitmap 上的,而这个 Bitmap 就是默认画布。

方式二:自建画布
有时候我们需要在特定的 Bitmap 上作画,比如给照片加水印;或者我们只需要一块空白画布。在这些情况下,我们就需要自己来创建 Canvas 对象。

1
2
3
Bitmap bitmap = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);

在上面的代码中,我们先创建一个空白的 Bitmap,然后再利用这个 Bitmap 创建一个 Canvas 对象,那么,调用 Canvas 的任何绘图函数最终都将画在这个 Bitmap 上。最后,我们可以将这个 Bitmap 保存到本地,也可以画到 View 上。

2. Bitmap 格式

我们都知道 Bitmap 是位图,也就是由一个个像素点组成的。所以,它肯定涉及两个问题:第一,如何存储每个像素点;第二,相关的像素点之间是否能够压缩,这也就涉及压缩算法的问题。

1)如何存储每个像素点

一张位图所占用的内存 = 图片长度(px) x 图片宽度(px) x 一个像素点占用的字节数。在 Android 中,存储一个像素点所使用的字节数是用枚举类型 Bitmap.Config 中的各个参数来表示的,如下图所示。

其中,A 代表透明度;R 代表红色;G 代表绿色;B 代表蓝色。

  • ALPHA_8:表示 8 位 Alpha 位图,即 A = 8,表示只存储 Alpha 位,不存储颜色值。一个像素点占用 1 字节。它没有颜色,只有透明度。
  • ARGB_4444:表示 16 位 ARGB 位图,即 A、R、G、B 各占 4 位,一个像素点占 4 + 4 + 4 + 4 = 16 位,2 字节。
  • ARGB_8888:表示 32 位 ARGB 位图,即 A、R、G、B 各占 8 位,一个像素点占 8 + 8 + 8 + 8 = 32 位,4 字节。
  • RGBA_F16:表示 64 位 RGBA 位图,8 字节,Android 8.0 新增。
  • RGB_565:表示 16 位 RGB 位图,即 R 占 5 位,G 占 6 位,B 占 5 位,它没有透明度,一个像素点占 5 + 6 + 5 = 16 位,2 字节。

大家应该都知道,每个色值所占得位数越大,颜色越艳丽。为什么呢?

假设表示透明度的 A 占 4 位,我们来算一下,4 位的透明度有多少种取值?很明显,每位要么是 0,要么是 1,所以共有 $2^4$,也就是 16 种取值。假设透明度占 8 位呢?那么这个透明度就有 $2^8$,也就是 256 种取值。表示颜色值的 R、G、B 所占位数与颜色取值数的计算方式是一样的。很明显,取值数越多,所能表示的颜色就越多,颜色就越艳丽。

以上 5 种格式各自表示了以何种状态存储 Bitmap。ALPHA_8 格式只存储透明度,而不存储颜色值,由于所表示的内容太过简单,所以我们一般不用;RGB_565 格式只存储颜色值,而不存储透明度,透明度全部是 FF,假如对图片没有透明度要求,相比 ARGB_8888 格式将节省一半的内存开销;其他三种格式都是既存储透明度又存储颜色值,但 ARGB_4444 格式的画质惨不忍睹,在 API 13 中已经被弃用了。RGBA_F16 格式是最占内存的,同时也是画质最高的。如果对画质没那么高的要求,一般用 ARGB_8888 格式。

下面我们来看一下如何计算 Bitmap 所占的内存大小。

在讲解 Bitmap 所占内存大小之前,我们先明确一个概念:内存中存储的 Bitmap 对象与文件中存储的 Bitmap 图片不是一个概念。文件中存储的 Bitmap 图片是经过我们在后面降到的压缩算法压缩过得;而内存中存储的 Bitmap 对象是通过 BitmapFactory 或者 Bitmap 的 Create 方法创建的,它保存在内存中,而且具有明确的宽和高。所以,很明显,内存中存储的一个 Bitmap 对象,它所占的内存大小 = Bitmap 的宽 x Bitmap 的高 x 每个像素所占内存大小。

很多读者一旦需要画布,就会创建一个全屏幕大小的 Bitmap 作为画布。我们现在就来算一下在一个分辨率是 1024 像素 x 768 像素的屏幕上,创建一个与屏幕同样大小的 Bitmap,到底需要多少内存?也就是说,这个屏幕长度上有 1024 个像素,宽度上有 768 个像素。我们假设每个像素使用 ARGB_8888 格式来存储,也就是一个像素占 32 位,那么要全屏显示这张图片所占的内存大小 = 1024 x 768 x 32B = 25 165 824B = 24MB。全屏显示一张图片要用 24MB。而且更恐怖的是,有些人还会循环创建。这也是在有些人自定义的控件中经常出现 OOM 的原因。所以,我们在创建画布时,应尽量根据需要的大小来创建。

2)Bitmap 压缩格式

在 Android 中,压缩格式使用枚举类 Bitmap.CompressFormat 中的成员变量表示,如下图所示。

其实这个参数很简单,就是指定 Bitmap 是以 JPEG、PNG 还是 WEBP 格式来压缩的,每种格式对应一种压缩算法。有关各种压缩算法的具体效果,我们会在 10.2.5 节中具体讲解。

10.2.2 创建 Bitmap 方法之一:BitmapFactory

BitmapFactory 用于从各种资源、文件、数据流和字节数组中创建 Bitmap(位图)对象。BitmapFactory 类是一个工具类,提供了大量的函数,这些函数可用于从不同的数据源中解析、创建 Bitmap(位图)对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeResource(Resources res, int id, Options opts)
public static Bitmap decodeFile(String pathName)
public static Bitmap decodeFile(String pathName, Options opts)
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd)
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts)

单从这些函数中就可以看出,BitmapFactory 的功能很强大,可以针对资源、文件、字节数组、FileDescriptor 和 InputStream 数据流解析出对应的 Bitmap 对象,如果解析不出来,则返回 null。而且每个函数都有两个实现,两个实现之间只差一个 Options opts 参数(详见 10.2.3 节中讲述)。

1. decodeResource(Resources res, int id)

这个函数表示从资源中解码一张位图,主要以 R.drawable.xxx 形式从本地资源中加载。

  • Resources res:包含图像数据的资源对象,一般通过 Context.getResource() 函数获得。
  • int id:包含图像数据的资源 id。

示例代码:

1
2
3
4
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.head_icon);
ImageView iv = findViewById(R.id.img);
iv.setImageBitmap(bitmap);

2. decodeFile(String pathName)

这个函数的主要作用是通过文件路径来加载图片。在实际中,一般在从相册中加载图片或者拍照使用,首先通过 intent 打开相册或摄像头,然后通过 onActivityResult() 函数获取图片 URI,再根据 URI 获取图片路径,最后根据路径解析出图片。其过程详见 拍照、相册及裁剪的终极实现系列

  • String pathName:解码文件的全路径名。必须是全路径名。

使用示例:

1
2
3
4
5
String fileName = "/data/data/demo.jpg";
Bitmap bmp = BitmapFactory.decodeFile(fileName);
if (bmp == null) {
// TODO 文件不存在
}

3. decodeByteArray(byte[] data, int offset, int length)

根据 Byte 数组来解析出 Bitmap。

  • byte[] data:压缩图像数据的字节数组。
  • int offset:图像数据偏移量,用于解码器定位从哪里开始解析。
  • int length:字节数,从偏移量开始,指定取多少字节进行解析。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
final ImageView iv = findViewById(R.id.img);
// 1. 开启异步线程去获取网络图片
new Thread(new Runnable() {
@Override
public void run() {
try {
// 2. 将网络返回的 InputStream 转换成 byte[]
byte[] data = getImage(path);
int length = data.length;
// 3. 解析
final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, length);
iv.post(new Runnable() {
@Override
public void run() {
iv.setImageBitmap(bitmap);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
public static byte[] getImage(String path) throws Exception {
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setReadTimeout(6 * 1000);
InputStream in = null;
if (connection.getResponseCode() == 200) {
in = connection.getInputStream();
byte[] result = readStream(in);
in.close();
return result;
}
return null;
}
public static byte[] readStream(InputStream in) throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = in.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
outputStream.close();
in.close();
return outputStream.toByteArray();
}

因为 BitmapFactory.decodeByteArray() 函数所需的 data 字节数组并不是想象中的数组,而是把输入流转换成字节内存输出流的字节数组格式。如果不经过 OutputStream 转换,直接返回从 InputStream 中读取到的 byte 数组,那么 decodeByteArray() 函数将一直返回 null。

4. decodeFileDescriptor

有两个构造函数,其参数:

  • FileDescriptor fd:包含解码位图数据的文件路径。
  • Rect outPadding:用于返回矩形的内边距。如果 Bitmap 没有被解析成功,则返回 (-1, -1, -1, -1);如果不需要,则可以传入 null。这个参数一般不使用。

示例代码:

1
2
3
4
5
6
String path = "/data/data/demo.jpg";
FileInputStream is = new FileInputStream(path);
bmp = BitmapFactory.decodeFileDescriptor(is.getFD());
if (bmp == null) {
// TODO 文件不存在
}

在 Android 老版本中,BitmapFactory.decodeFileDescriptor() 解析方法比使用 BitmapFactory.decodeFile(path) 更节省内存。对比源码发现,前者是直接调用 nativeDecodeFileDescriptor() 函数,它是 Android Native 里的函数,被封装在 SO 里;而追踪 decodeFile() 函数发现,在最终调用 nativeDecodeStream() 函数之前,最多可能会申请两次空间。在 API 28 中源码没有发现多处申请内存空间的问题。

5. decodeStream

1
2
public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
  • InputStream is:用于解码位图的原始输入流。
  • Rect outPadding:用于返回矩形的内边距。如果 Bitmap 没有被解析成功,则返回 (-1, -1, -1, -1);如果不需要,则可以传入 null。这个参数一般不使用。

对前面 decodeByteArray 示例代码进行改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream inputStream = getImage(path);
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
iv.post(new Runnable() {
@Override
public void run() {
iv.setImageBitmap(bitmap);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
public static InputStream getImage(String path) throws Exception {
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setReadTimeout(6 * 1000);
InputStream in = null;
if (connection.getResponseCode() == 200) {
return connection.getInputStream();
}
return null;
}

10.2.3 BitmapFactory.Options

这个参数的作用非常大,它可以设置 Bitmap 的采样率,通过改变图片的宽度、高度、缩放比例等,以减少图片的像素的目的。总的来说,通过设置这个值,可以更好地控制、显示、使用 Bitmap。在实际开发中可以灵活使用该值,以降低 OOM 的发生概率。

下面列出常用的部分成员变量。

1
2
3
4
5
6
7
8
9
10
public boolean inJustDecodeBounds;
public int inSampleSize;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity;
public Bitmap.Config inPreferredConfig;
public int outWidth;
public int outHeight;
public String outMimeType;

以 in 开头的代表的就是设置某某参数;以 out 开头的代表的就是获取某某参数。比如,inSampleSize 就是设置 Bitmap 的缩放比例,outWidth 就是获取 Bitmap 的高度。

1. inJustDecodeBounds 获取图片信息

将这个字段设置为 true,则表示只解析图片信息,不获取图片,不分配内存。能获取的信息有图片的宽度、高度和图片的 MIME 类型。图片的宽度、高度通过 options.outWidth (图片的原始宽度) 和 options.outHeight (图片的原始高度) 返回;图片的 MIME 类型通过 options.outMimeType 返回。

1
2
3
4
5
6
7
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head, options);
Log.d("xian", "bitmap: " + bitmap);
Log.d("xian", "realWidth: " + options.outWidth +
", realHeight: " + options.outHeight +
", mimeType: " + options.outMimeType);

从结果中看可以看出,返回的 Bitmap 是 null,而获取到的 width 和 height 都是有值的。这就证明了我们的结论:inJustDecodeBounds 只会解析 Bitmap 的宽/高参数,而不会解析 Bitmap,整个过程是不占内存的。

2. inSampleSize 压缩图片

这个字段表示采样频率,简称采样率,是指每隔多少个样本采样一次作为结果。比如,将这个字段设置为 4,意思就是从原本图片的 4 个像素中取一个像素作为结果返回,其余的都丢弃,这样,结果图片的宽和高都为原来的 1/4。同样,如果将这个字段设置为 16,意思就是从每 16 个像素中取一个像素返回,同样,宽和高都为原来的 1/16。很明显,采样率越大,图片越小,同时图片越失真。

针对 inSampleSize 的值,官方建议取 2 的冥数,比如 1、2、4、8、16 等,否则会被系统向下取整并找到一个最接近的值。不能去小于 1 的值,否则系统将一直使用 1 来作为采样率。

所以,这个参数主要用来对图像进行压缩。那如何确定一张图片的采样率呢?那就是使得缩放后的图片尺寸尽量大于等于相应的 ImageView 大小。一般计算 inSampleSize 的步骤如下。

第一步,获取图片的原始宽高。通过将 Options 对象的 inJustDecodeBounds 属性设置为 true 后调用 decodeResource() 函数,可以实现不真正加载图片而只获取图片的尺寸信息。代码如下:

1
2
3
4
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), resId, options);
// 现在原始宽高存储在 Options 对象的 outWidth 和 outHeight 实例域中

第二步,根据原始宽高和目标宽高计算出 inSampleSize。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// dstWidth 和 dstHeight 分为被目标 ImageView 的宽和高
public static int calSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight) {
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int inSampleSize = 1;
if (rawWidth > dstWidth || rawHeight > dstHeight) {
float ratioWidth = (float) rawWidth / dstWidth;
float ratioHeight = (float) rawHeight / dstHeight;
inSampleSize = (int) Math.min(ratioWidth, ratioHeight);
}
return inSampleSize;
}

第三步,根据采样率解析出压缩后的 Bitmap。代码如下:

1
2
3
4
5
6
7
8
BitmapFactory.Options options2 = new BitmapFactory.Options();
options2.inSampleSize = sampleSize;
try {
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.scenery, options2);
iv.setImageBitmap(bmp);
} catch (OutOfMemoryError err) {
// TODO OOM
}

3. 加载一个 Bitmap 文件究竟要占多少空间

为了适配不同的屏幕,Android 系统预先准备了几个资源文件夹。

文件夹 drawable-ldpi drawable-mdpi drawable-hdpi drawable-xhdpi drawable-xxhdpi drawable-xxxhdpi
density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640
  • density:表示 dpi 与 px 的换算比例。
  • densityDpi:表示在对应的分辨率下每英寸有多少个 dpi。

即:屏幕上 1 英寸长所对应的 px 数 = density × densityDpi。Android 系统在加载图片时会根据需要动态缩放图片所占的像素数,也就是会动态缩放图片的尺寸。

比如,有一张 640px × 800px 的图片存放在 xhdpi 文件夹下,这个文件夹所对应的屏幕分辨率是 480dpi,而当真实的屏幕分辨率是 720dpi 的时候,就需要放大此图,以适配这个屏幕,放大倍数就是 720 / 480 = 1.5。加载到内存时,Bitmap 对象的尺寸是 960px × 1200px。因为 Bitmap 默认使用 ARGB_8888格式来存储,也就是每个像素占 4 个字节,所以实际所占得内存字节数为 640px × 1.5 × 800px × 1.5 × 4 = 4608000。但是从 SD 卡加载同样的图片,就不会进行缩放,所占的内存为 640px × 800px × 4 = 2048000。

  • 不同名称的资源文件夹是为了适配不同的屏幕分辨率的,当屏幕分辨率与文件所在资源文件夹对应的分辨率相同时,直接使用图片,不会对图片进行缩放。
  • 当屏幕分辨率与图片所在文件夹所对应的分辨率不同时,会进行缩放,缩放比例是:屏幕分辨率 / 文件夹所对应的分辨率。
  • 当从本地文件夹中加载图片时,不会对图片进行缩放。

4. inScaled、inDensity、inTargetDensity、inScreenDensity

  • inScaled:在需要缩放时,是否对当前文件进行缩放。值为 false 表示不缩放;值为 true 或者不设置,则会根据文件夹分辨率和屏幕分辨率动态缩放。默认为 true。
  • inDensity:用于设置文件所在资源文件夹的屏幕分辨率。
  • inTargetDensity:表示真实显示的屏幕分辨率。
  • inScreenDensity:在源码中没有用到此参数,不表。

一张图片的缩放比例是通过屏幕真实的分辨率 / 所在资源文件夹所对应的分辨率得出来的,在这里,也就是缩放比例 scale = inTargetDensity / inDensity。这俩个参数的作用就是:可以通过手动设置文件所在资源文件夹的分辨率和真实显示的屏幕分辨率来指定图片的缩放比例。

5. inPreferredConfig

这个参数用来设置像素的存储格式的。

1
options.inPreferredConfig = Bitmap.Config.RGB_565;

10.2.4 创建 Bitmap 方法之二:Bitmap 静态方法

1
2
3
4
5
6
7
8
9
10
11
12
static Bitmap createBitmap(int width, int height, Bitmap.Config config)
static Bitmap createBitmap(int[] colors, int width, int height, Bitmap.Config config)
static Bitmap createBitmap(int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)
static Bitmap createBitmap(Bitmap src)
static Bitmap createBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)
static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)
// 在 API 17 中添加
static Bitmap createBitmap(DisplayMetrics display, int width, int height, Bitmap.Config config)
static Bitmap createBitmap(DisplayMetrics display, int[] colors, int width, int height, Bitmap.Config config)
static Bitmap createBitmap(DisplayMetrics display, int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)

1. static Bitmap createBitmap(int width, int height, Bitmap.Config config)

这个函数可以创建一幅指定大小的空白图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class TestView extends View {
private Bitmap mDestBmp;
private Paint mPaint;
public TestView(Context context) {
super(context);
init();
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TestView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mPaint = new Paint();
int width = 500;
int height = 300;
mDestBmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mDestBmp);
Paint paint = new Paint();
LinearGradient linearGradient = new LinearGradient(width / 2f, 0, width / 2f,
height, 0xffffffff, 0x00ffffff, Shader.TileMode.CLAMP);
paint.setShader(linearGradient);
canvas.drawRect(0, 0, width, height, paint);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mDestBmp, 0, 0, mPaint);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
canvas.drawRect(0, 0, mDestBmp.getWidth(), mDestBmp.getHeight(), mPaint);
}
}

2. createBitmap(Bitmap source, int x, int y, int width, int height)

这个函数主要用于裁剪图像,各参数的含义如下。

  • Bitmap source:用于裁剪的源图像。
  • int x, y:开始裁剪的位置点坐标。
  • int width, height:裁剪的宽度和高度。
1
2
Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
Bitmap cuteBmp = Bitmap.createBitmap(src, src.getWidth() / 3, src.getHeight() / 3, src.getWidth() / 3, src.getHeight() / 3)

这里只是将图像裁剪成矩形。若想把图像裁剪成圆形或者椭圆形,不是使用 Bitmap 的自带方法,而需要用到 Xfermode 图像混合的知识,详见 8.3.2 节。

3. createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)

这个函数相比上面的裁剪函数多了两个参数:Matrix m 和 boolean filter。它的作用也很明显,就是不仅能实现裁剪,还能给裁剪后的图像添加矩阵。

  • Matrix m:给裁剪后的图像添加矩阵。
  • boolean filter:对应 paint.setFilterBitmap(filter),即是否给图像添加滤波效果。如果设置为 true,则能够减少图像中由于噪声引起的突兀的孤立像素点或像素块。
1
2
3
4
Matrix matrix = new Matrix();
matrix.setScale(2, 1);
Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
Bitmap cuteBmp = Bitmap.createBitmap(src, src.getWidth() / 3, src.getHeight() / 3, src.getWidth() / 3, src.getHeight() / 3, matrix, true)

将裁剪后的小狗宽度方向放大两倍。

4. createBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)

该函数用于缩放 Bitmap。各参数的含义如下。

  • Bitmap src:需要缩放的源图像。
  • int dstWidth, dstHeight:缩放后的目标宽高。
  • boolean filter:是否给图像添加滤波效果,对应 paint.setFilterBitmap(filter)

5. 建议

在加载或创建 Bitmap 时,必须如下面代码所示,通过 try…catch 语句捕捉 OutOfMemoryError,以防出现 OOM 问题。

1
2
3
4
5
6
try {
Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
Bitmap bitmap = Bitmap.createScaledBitmap(src, 300, 200, true);
} catch (OutOfMemoryError error) {
error.printStackTrace();
}

10.2.5 常用函数

1. copy(Config config, boolean isMutable)

根据源图像创建一个副本,可以指定副本的像素存储格式。

  • Config config:像素在内存中的存储格式。取值为 ARGB_8888等。
  • boolean isMutable:新创建的 Bitmap 是否可以更改其中的像素。

我们可以使用下面的方法来判断当前的 Bitmap 是不是像素可更改的。

1
boolean isMutable();

返回 true 表示像素可以更改的。如果像素是不可更改的,但仍要使用 setPixel() 等函数设置 Bitmap 中的像素值时,就会报错。通过 BitmapFactory 加载的 Bitmap 都是像素不可更改的,只有通过 Bitmap 中的几个函数创建的 Bitmap 才是像素可更改的。这些函数如下:

1
2
3
4
5
6
copy(Bitmap.Config config, boolean isMutable)
createBitmap(int width, int height, Bitmap.Config config)
// 当指定的目标缩放宽高与源图像宽高一样时,就会返回源图像,如果源图像是像素不可更改的,那么返回的图像依然是不可更改的
// 进行缩放过才会生成新的图像,而新生成的图像是像素可更改的。
createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
createBitmap(DisplayMetrics display, int width, int height, Bitmap.Config config)

对于像素不可更改的图像,是不能作为画布的,比如下面的代码就会报错:

1
2
3
Bitmap srcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
Canvas canvas = new Canvas(srcBmp);
canvas.drawColor(Color.RED);

显然,srcBmp 是像素不可更改的,然而,当其作为 Canvas 以后,如果要向其中填充颜色,则必然会改变它的像素值,肯定为报错。

2. extractAlpha()

这个函数的主要作用是从 Bitmap 中抽取出 Alpha 值,生成一幅只含有 Alpha 值的图像,像素存储格式是 ALPHA_8。它有两个构造函数。

1)Bitmap extractAlpha()

示例:将图像的透明通道抽取出来,并染成天蓝色。

1
2
3
4
5
6
7
8
9
10
11
Bitmap srcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.cat_dog);
Bitmap bitmap = Bitmap.createBitmap(srcBmp.getWidth(), srcBmp.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.CYAN);
canvas.drawBitmap(srcBmp.extractAlpha(), 0, 0, paint);
ImageView iv = findViewById(R.id.img);
iv.setImageBitmap(bitmap);
srcBmp.recycle();
2)Bitmap extractAlpha(Paint paint, int[] offsetXY)
  • Paint paint:具有 MaskFilter 效果的 Paint 对象,一般使用 BlurMaskFilter 模糊效果。
  • int[] offsetXY:返回在添加 BlurMaskFilter 效果以后原点的偏移量。比如,我们使用一个半径为 6 的 BlurMaskFilter 效果,那么在源图像被模糊以后,图像的上下左右 4 条边都会多出 6px 的模糊效果。所以,要想完全显示这幅图像,就不应该从源图像左上角 (0, 0) 点开始绘制,而应从 (-6, -6) 点开始绘制,而 offsetXY 就是相对源图像的建议绘制起始位置,所以此时 offsetXY 的值就是 [-6, -6]。注意,offsetXY 只是建议的绘制起始位置,其取值并不一定与 BlurMaskFilter 的模糊半径一致。

利用这个模糊效果,可以实现发光效果,如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
try {
Bitmap srcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.cat_dog);
// 获取 Alpha Bitmap
Paint alphaPaint = new Paint();
BlurMaskFilter blurMaskFilter = new BlurMaskFilter(20, BlurMaskFilter.Blur.NORMAL);
alphaPaint.setMaskFilter(blurMaskFilter);
int[] offsetXY = new int[2];
Bitmap alphaBmp = srcBmp.extractAlpha(alphaPaint, offsetXY);
// 创建 Bitmap
Bitmap bitmap = Bitmap.createBitmap(alphaBmp.getWidth(), alphaBmp.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.CYAN);
canvas.drawBitmap(alphaBmp, 0, 0, paint);
// 绘制源图像
canvas.drawBitmap(srcBmp, -offsetXY[0], -offsetXY[1], null);
// 设置图像并回收没用的资源
ImageView iv = findViewById(R.id.img);
iv.setImageBitmap(bitmap);
srcBmp.recycle();
} catch (OutOfMemoryError error) {
error.printStackTrace();
}
}
}
3)示例:单击描边效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class TestView extends AppCompatImageView {
public TestView(Context context) {
super(context);
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TestView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Paint p = new Paint();
p.setColor(Color.CYAN);
setStateDrawable(this, p);
}
private void setStateDrawable(ImageView view, Paint paint) {
// 拿到源图像
BitmapDrawable bd = (BitmapDrawable) view.getDrawable();
Bitmap srcBmp = bd.getBitmap();
// 制作纯色背景
Bitmap bitmap = Bitmap.createBitmap(srcBmp.getWidth(), srcBmp.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(srcBmp.extractAlpha(), 0, 0, paint);
// 添加动态
StateListDrawable sld = new StateListDrawable();
sld.addState(new int[]{android.R.attr.state_pressed}, new BitmapDrawable(bitmap));
// setBackgroundDrawable() 函数会移除原有的 padding 值。
// 如果需要 padding,则需要调用 setPadding() 函数。
view.setBackgroundDrawable(sld);
}
}

3. 分配空间获取

1
2
3
4
5
6
7
8
// API 19,获取 Bitmap 所分配的内存
int getAllocationByteCount()
// API 12,获取 Bitmap 所分配的内存
int getByteCount()
// API 1,获取每行所分配的内存大小。Bitmap 所占内存 = getRowBytes() × bitmap.getHeight()。
int getRowBytes()

所以,一般计算 Bitmap 内存占用的函数会写成如下这样:

1
2
3
4
5
6
7
8
9
10
11
12
public int getBitmapSize(Bitmap bitmap) {
if (bitmap == null) {
return 0;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return bitmap.getAllocationByteCount();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
return bitmap.getRowBytes() * bitmap.getHeight();
}

4. recycle()、isRecycled()

这是两个与图片回收有关的函数,其声明如下:

1
2
3
4
5
// 强制回收 Bitmap 所占的内存
public void recycle()
// 判断当前 Bitmap 的内存是否被回收
public final boolean isRecycled()

所以,如果要回收内存,则代码一般这样写:

1
2
3
4
5
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
System.gc();
}

注意:使用内存已经被回收的 Bitmap 引起 Crash;在 API 10 及以前的版本中,必须强制调用 recycle() 函数来释放内存;从 API 11 开始,不再强制调用 recycle() 函数来释放内存。

5. setDensity()、getDensity()

在 BitmapFactory 中,我们讲过几个 Density 值,如 inDensity、inTargetDensity,而这里 Bitmap 的 setDensity()、getDensity() 函数所对应的就是 inDensity。inDensity 用于表示该 Bitmap 适合的屏幕 dpi,当目标屏幕的 dpi (inTargetDensity) 不等于它时,将会缩放图像以适应目标机器。

6. setPixel()、getPixel()

这两个函数用于针对 Bitmap 中某个位置的像素进行设置和获取。举个例子:将图片中的绿色通道增大 30,如下图所示。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
Bitmap srcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
ImageView iv1 = findViewById(R.id.img1);
iv1.setImageBitmap(srcBmp);
Bitmap desBmp = srcBmp.copy(Bitmap.Config.ARGB_8888, true);
for (int h = 0; h < srcBmp.getHeight(); h++) {
for (int w = 0; w < srcBmp.getWidth(); w++) {
int originColor = srcBmp.getPixel(w, h);
int alpha = Color.alpha(originColor);
int red = Color.red(originColor);
int green = Color.green(originColor);
int blue = Color.blue(originColor);
if (green < 225) {
green += 30;
}
desBmp.setPixel(w, h, Color.argb(alpha, red, green, blue));
}
}
ImageView iv2 = findViewById(R.id.img2);
iv2.setImageBitmap(desBmp);
}
}

7. compress()

1)概述

用于压缩图像,它会将压缩过得 Bitmap 写入指定的输出流中。函数声明如下:

1
public boolean compress(CompressFormat format, int quality, OutputStream stream)

  • CompressFormat format:压缩格式,取值有:CompressFormat.JPEG、CompressFormat.PNG、CompressFormat.WEBP (API 14)。
  • int quality:表示压缩后图像的画质,取值是 0~100。0 表示 以最低画质压缩,100 表示以最高画质压缩。对于 PNG 等无损格式的图片,会忽略此项设置。
  • OutputStream stream:这是输出值,Bitmap 在被压缩后,会以 OutputStream 的形式在这里输出。
  • 返回值 boolean:当压缩成功后,返回 true;失败则返回 false。
2)压缩格式
  • CompressFormat.JPEG: 采用 JPEG 压缩算法,是一种有损压缩方式,即在压缩过程中会改变图像的原本质量。compress() 函数中的 quality 参数值越小,画质越差,对图片的原有质量损伤越大,但是得到的图片文件比较小。而且,JPEG 不支持 Alpha 透明度,当遇到透明度像素时,会以黑色背景填充。
  • CompressFormat.PNG:采用 PNG 压缩算法,是一种支持透明度的无损压缩格式。
  • CompressFormat.WEBP:WEBP 是一种同时提供了有损压缩与无损压缩的图片文件格式,派生自视频编码格式 VP8;从 Android 4.0(API 14)开始支持 WEBP,从 Android 4.2.1+(API 18)开始支持无损 WEBP 和带 Alpha 通道的 WEBP。从整体来讲,WEBP 格式是通过牺牲压缩时间来减小产出文件大小的。
3)压缩图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
ImageView iv1 = findViewById(R.id.img1);
final ImageView iv2 = findViewById(R.id.img2);
final Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.head);
iv1.setImageBitmap(bmp);
new Thread(new Runnable() {
@Override
public void run() {
// 压缩图像后,显示
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.JPEG, 1, bos);
byte[] bytes = bos.toByteArray();
final Bitmap bmp1 = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
iv2.post(new Runnable() {
@Override
public void run() {
iv2.setImageBitmap(bmp1);
}
});
}
}).start();
}
}
4)示例:保存压缩后的图像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void saveBitmap(Bitmap bitmap) {
File fileDir = Environment.getExternalStorageDirectory();
String path = fileDir.getAbsolutePath() + "/xian.jpeg";
File file = new File(path);
if (file.exists()) {
file.delete();
}
try {
FileOutputStream outputStream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 10, outputStream);
outputStream.flush();
outputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

10.2.6 常见问题

1. 对 Bitmap 的画笔设置 ANTI_ALIAS_FLAG 属性,为什么无效

简单来说,ANTI_ALIAS_FLAG 属性通过混合前景色与背景色来产生平滑的边缘。比如背景色是透明的,而前景色是红色的,ANTI_ALIAS_FLAG 属性通过将边缘处的像素由纯色逐步转换为透明来让边缘看起来是平滑的。

而当我们在 Bitmap 上重绘时,像素的颜色会越来越纯粹,从而导致边缘越来越粗糙。所以,可以有两种选择即可避免设置 ANTI_ALIAS_FLAG 属性无效的问题:

  • 避免重绘。
  • 在重绘前清空 Bitmap。

避免重绘的方法很简单,只需要保证让 Bitmap 只被绘制一次即可,比如将 Bitmap 绘制操作放在初始化的时候,而不要放在可能被多次调用的 onDraw()、onMeasure()、onLayout() 等函数中。

清空 Bitmap 可以参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Bitmap mBitmap;
private Canvas mCanvas;
private void init() {
mBitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 清空 Bitmap
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// ...
mCanvas.drawXXX();
}

2. 如何生成水印

其实原理很简单,新生成一个 Bitmap,先后将源 Bitmap 和水印 Bitmap 画上去即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.meinv);
Bitmap waterBitmap = getWaterBitmap();
Bitmap bitmap = createWaterBitmap(srcBitmap, waterBitmap);
ImageView imageView = findViewById(R.id.img);
imageView.setImageBitmap(bitmap);
}
/**
* 添加水印
* @param srcBitmap 源图
* @param waterBitmap 水印图
* @return 添加水印的图像
*/
private Bitmap createWaterBitmap(Bitmap srcBitmap, Bitmap waterBitmap) {
if (srcBitmap == null) {
return null;
}
if (waterBitmap == null) {
return srcBitmap;
}
int w = srcBitmap.getWidth();
int h = srcBitmap.getHeight();
int ww = waterBitmap.getWidth();
int wh = waterBitmap.getHeight();
// 创建空白图像,宽高等同 srcBitmap
Bitmap newBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(newBitmap);
// 画原图,从 (0, 0) 坐标开始
canvas.drawBitmap(srcBitmap, 0, 0, null);
// 在原图的右下角画入水印
canvas.drawBitmap(waterBitmap, w - ww + 10, h - wh + 10, null);
return newBitmap;
}
/**
* 水印
* @return Bitmap
*/
private Bitmap getWaterBitmap() {
return makeTextBitmap("先小涛", 200, Color.GRAY);
}
/**
* 将字符串转化成 bitmap
* @return Bitmap
*/
private Bitmap makeTextBitmap(String text, int size, int color) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(size);
paint.setColor(color);
paint.setTextAlign(Paint.Align.LEFT);
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int width = (int) paint.measureText(text);
int height = fm.descent - fm.ascent;
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawText(text, 0, fm.leading - fm.ascent, paint);
canvas.save();
return bitmap;
}
}